﻿using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
using System.Web.Security;
using reDoo.Data;
using reDoo.Model;
using StructureMap;

namespace reDoo.Security
{
    public class OpenMembershipProvider : MembershipProvider
    {
        #region Declarations / Fields

        private readonly IMemberRepository memberRepository;
        private int maxInvalidPasswordAttempts;
        private int passwordAttemptWindow;
        private int minRequiredNonAlphanumericCharacters;
        private int minRequiredPasswordLength;
        private string passwordStrengthRegularExpression;
        private bool enablePasswordReset;
        private bool enablePasswordRetrieval;
        private bool requiresQuestionAndAnswer;
        private bool requiresUniqueEmail;
        private MembershipPasswordFormat passwordFormat;
        private MachineKeySection machineKey;

        private const string DEFAULT_PROVIDER_NAME = "reDoo OpenMembershipProvider";
        private const string DEFAULT_PROVIDER_DESCRIPTION = "Membership provider implementations that can be used for custom data stores.";
        private const int DEFAULT_NEW_PASSWORD_LENGTH = 8;

        #endregion

        #region Constructors

        public OpenMembershipProvider()
            : this(ObjectFactory.GetInstance<IMemberRepository>())
        {
        }

        public OpenMembershipProvider(IMemberRepository memberRepository)
        {
            this.memberRepository = memberRepository;
        }

        #endregion

        #region Initialization

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            if (string.IsNullOrEmpty(name))
                name = DEFAULT_PROVIDER_NAME;

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", DEFAULT_PROVIDER_DESCRIPTION);
            }

            base.Initialize(name, config);

            this.ParseConfigurationSettings(config);

            this.ParsePasswordFormat(config);

            Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            if (machineKey.ValidationKey.Contains("AutoGenerate"))
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                    throw new ProviderException("Hashed or Encrypted passwords " +
                                                "are not supported with auto-generated keys.");

        }

        private void ParseConfigurationSettings(NameValueCollection config)
        {
            this.ApplicationName = GetConfigValue(config["applicationName"], "/");
            this.maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            this.passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            this.minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
            this.minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            this.passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
            this.enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            this.enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            this.requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
            this.requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));
        }

        private static string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
                return defaultValue;

            return configValue;
        }

        private void ParsePasswordFormat(NameValueCollection config)
        {
            string temp_format = config["passwordFormat"] ?? "Hashed";

            switch (temp_format)
            {
                case "Hashed":
                    this.passwordFormat = MembershipPasswordFormat.Hashed;
                    break;
                case "Encrypted":
                    this.passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;
                case "Clear":
                    this.passwordFormat = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
            }
        }

        #endregion

        #region Config value properties

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to retrieve their passwords.
        /// </summary>
        /// <returns>
        /// true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.
        /// </returns>
        public override bool EnablePasswordRetrieval
        {
            get { return this.enablePasswordRetrieval; }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to reset their passwords.
        /// </summary>
        /// <returns>
        /// true if the membership provider supports password reset; otherwise, false. The default is true.
        /// </returns>
        public override bool EnablePasswordReset
        {
            get { return this.enablePasswordReset; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
        /// </summary>
        /// <returns>
        /// true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresQuestionAndAnswer
        {
            get { return this.requiresQuestionAndAnswer; }
        }

        /// <summary>
        /// The name of the application using the custom membership provider.
        /// </summary>
        /// <returns>
        /// The name of the application using the custom membership provider.
        /// </returns>
        public override string ApplicationName
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// The number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </returns>
        public override int MaxInvalidPasswordAttempts
        {
            get { return this.maxInvalidPasswordAttempts; }
        }

        /// <summary>
        /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </returns>
        public override int PasswordAttemptWindow
        {
            get { return passwordAttemptWindow; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
        /// </summary>
        /// <returns>
        /// true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresUniqueEmail
        {
            get { return this.requiresUniqueEmail; }
        }

        /// <summary>
        /// Gets a value indicating the format for storing passwords in the membership data store.
        /// </summary>
        /// <returns>
        /// One of the <see cref="T:System.Web.Security.MembershipPasswordFormat"/> values indicating the format for storing passwords in the data store.
        /// </returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { return this.passwordFormat; }
        }

        /// <summary>
        /// Gets the minimum length required for a password.
        /// </summary>
        /// <returns>
        /// The minimum length required for a password. 
        /// </returns>
        public override int MinRequiredPasswordLength
        {
            get { return this.minRequiredPasswordLength; }
        }

        /// <summary>
        /// Gets the minimum number of special characters that must be present in a valid password.
        /// </summary>
        /// <returns>
        /// The minimum number of special characters that must be present in a valid password.
        /// </returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return this.minRequiredNonAlphanumericCharacters; }
        }

        /// <summary>
        /// Gets the regular expression used to evaluate a password.
        /// </summary>
        /// <returns>
        /// A regular expression used to evaluate a password.
        /// </returns>
        public override string PasswordStrengthRegularExpression
        {
            get { return this.passwordStrengthRegularExpression; }
        }

        #endregion

        #region Create user

        /// <summary>
        /// Adds a new membership user to the data source.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the information for the newly created user.
        /// </returns>
        /// <param name="username">The user name for the new user. </param><param name="password">The password for the new user. </param><param name="email">The e-mail address for the new user.</param><param name="passwordQuestion">The password question for the new user.</param><param name="passwordAnswer">The password answer for the new user</param><param name="isApproved">Whether or not the new user is approved to be validated.</param><param name="providerUserKey">The unique identifier from the membership data source for the user.</param><param name="status">A <see cref="T:System.Web.Security.MembershipCreateStatus"/> enumeration value indicating whether the user was created successfully.</param>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            status = this.ValidateUserCreationPrerequisites(username, password, email);
            MembershipUser user = null;

            if (MembershipCreateStatus.Success == status)
            {
                DateTime creationDate = DateTime.Now;

                var member = new Member
                                 {
                                     Id = (int)providerUserKey,
                                     Username = username,
                                     Password = EncodePassword(password),
                                     Email = email,
                                     PasswordQuestion = passwordQuestion,
                                     PasswordAnswer = EncodePassword(passwordAnswer),
                                     IsApproved = isApproved,
                                     CreationDate = creationDate,
                                     LastPasswordChangedDate = creationDate,
                                     LastActivityDate = creationDate,
                                     IsLockedOut = false,
                                     LastLockedOutDate = creationDate,
                                     FailedPasswordAttemptCount = 0,
                                     FailedPasswordAttemptWindowStart = creationDate,
                                     FailedPasswordAnswerAttemptCount = 0,
                                     FailedPasswordAnswerAttemptWindowStart = creationDate
                                 };


                try
                {
                    this.memberRepository.Add(member);
                    this.memberRepository.SaveChanges();
                    status = MembershipCreateStatus.Success;
                    user = ConvertToMembershipUser(member);
                }
                catch (Exception)
                {
                    status = MembershipCreateStatus.ProviderError;
                    throw;
                }

            }

            return user;
        }

        private MembershipCreateStatus ValidateUserCreationPrerequisites(string username, string password, string email)
        {
            MembershipCreateStatus validationResult = this.ValidatePasswordRequisites(username, password);

            if (validationResult != MembershipCreateStatus.Success)
                return validationResult;

            validationResult = this.ValidateEmailPrerequisites(email);

            if (validationResult != MembershipCreateStatus.Success)
                return validationResult;

            validationResult = this.ValidateUniqueUsername(username);

            return validationResult;
        }

        private MembershipCreateStatus ValidatePasswordRequisites(string username, string password)
        {
            var args = new ValidatePasswordEventArgs(username, password, true);
            MembershipCreateStatus validationResult = MembershipCreateStatus.Success;

            OnValidatingPassword(args);

            if (args.Cancel)
                validationResult = MembershipCreateStatus.InvalidPassword;

            return validationResult;
        }

        private MembershipCreateStatus ValidateEmailPrerequisites(string email)
        {
            MembershipCreateStatus validationResult = MembershipCreateStatus.Success;

            if (RequiresUniqueEmail && GetUserNameByEmail(email) != "")
                validationResult = MembershipCreateStatus.DuplicateEmail;

            return validationResult;
        }

        private MembershipCreateStatus ValidateUniqueUsername(string username)
        {
            MembershipCreateStatus validationResult = MembershipCreateStatus.Success;

            if (null != this.GetUser(username, false))
                validationResult = MembershipCreateStatus.DuplicateUserName;

            return validationResult;
        }

        #endregion

        #region Delete user

        /// <summary>
        /// Removes a user from the membership data source. 
        /// </summary>
        /// <returns>
        /// true if the user was successfully deleted; otherwise, false.
        /// </returns>
        /// <param name="username">The name of the user to delete.</param><param name="deleteAllRelatedData">true to delete data related to the user from the database; false to leave data related to the user in the database.</param>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            memberRepository.DeleteByUsername(username);
            memberRepository.SaveChanges();
            return true;
        }

        #endregion

        #region User validation

        /// <summary>
        /// Verifies that the specified user name and password exist in the data source.
        /// </summary>
        /// <returns>
        /// true if the specified username and password are valid; otherwise, false.
        /// </returns>
        /// <param name="username">The name of the user to validate. </param><param name="password">The password for the specified user. </param>
        public override bool ValidateUser(string username, string password)
        {
            bool isValid = false;

            var user = memberRepository.GetByUsername(username);
            if (null == user || user.IsLockedOut)
                return false;

            if (CheckPassword(password, user.Password))
            {
                if (user.IsApproved)
                {
                    isValid = true;
                    user.LastLoginDate = DateTime.Now;
                    memberRepository.SaveChanges();
                }
            }
            else
            {
                UpdateFailureCount(username, "password");
            }

            return isValid;

        }

        #endregion

        #region Change user data

        /// <summary>
        /// Processes a request to update the password question and answer for a membership user.
        /// </summary>
        /// <returns>
        /// true if the password question and answer are updated successfully; otherwise, false.
        /// </returns>
        /// <param name="username">The user to change the password question and answer for. </param><param name="password">The password for the specified user. </param><param name="newPasswordQuestion">The new password question for the specified user. </param><param name="newPasswordAnswer">The new password answer for the specified user. </param>
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            if (!ValidateUser(username, password))
                return false;

            var user = memberRepository.GetByUsername(username);
            user.PasswordQuestion = newPasswordQuestion;
            user.PasswordAnswer = newPasswordAnswer;
            memberRepository.SaveChanges();

            return true;

        }

        /// <summary>
        /// Processes a request to update the password for a membership user.
        /// </summary>
        /// <returns>
        /// true if the password was updated successfully; otherwise, false.
        /// </returns>
        /// <param name="username">The user to update the password for. </param><param name="oldPassword">The current password for the specified user. </param><param name="newPassword">The new password for the specified user. </param>
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (!ValidateUser(username, oldPassword))
                return false;

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Change password canceled due to new password validation failure.");

            var user = memberRepository.GetByUsername(username);
            user.Password = this.EncodePassword(newPassword);
            user.LastPasswordChangedDate = DateTime.Now;

            memberRepository.SaveChanges();

            return true;
        }

        /// <summary>
        /// Resets a user's password to a new, automatically generated password.
        /// </summary>
        /// <returns>
        /// The new password for the specified user.
        /// </returns>
        /// <param name="username">The user to reset the password for. </param><param name="answer">The password answer for the specified user. </param>
        public override string ResetPassword(string username, string answer)
        {
            if (!EnablePasswordReset)
                throw new NotSupportedException("Password reset is not enabled.");

            if (null == answer && RequiresQuestionAndAnswer)
            {
                UpdateFailureCount(username, "passwordAnswer");
                throw new ProviderException("Password answer required for password reset.");
            }

            string newPassword =
              Membership.GeneratePassword(DEFAULT_NEW_PASSWORD_LENGTH, MinRequiredNonAlphanumericCharacters);

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");


            var user = memberRepository.GetByUsername(username);


            if (null == user)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (user.IsLockedOut)
                throw new MembershipPasswordException("The supplied user is locked out.");

            string passwordAnswer = user.PasswordAnswer;

            if (RequiresQuestionAndAnswer && !CheckPassword(answer, passwordAnswer))
            {
                UpdateFailureCount(username, "passwordAnswer");
                throw new MembershipPasswordException("Incorrect password answer.");
            }

            user.Password = EncodePassword(newPassword);
            user.LastPasswordChangedDate = DateTime.Now;
            memberRepository.SaveChanges();

            return newPassword;
        }

        /// <summary>
        /// Updates information about a user in the data source.
        /// </summary>
        /// <param name="user">A <see cref="T:System.Web.Security.MembershipUser"/> object that represents the user to update and the updated information for the user. </param>
        public override void UpdateUser(MembershipUser user)
        {
            var repositoryUser = memberRepository.GetByUsername(user.UserName);

            repositoryUser.Email = user.Email;
            repositoryUser.Comment = user.Comment;
            repositoryUser.IsApproved = user.IsApproved;

            memberRepository.SaveChanges();
        }

        /// <summary>
        /// Clears a lock so that the membership user can be validated.
        /// </summary>
        /// <returns>
        /// true if the membership user was successfully unlocked; otherwise, false.
        /// </returns>
        /// <param name="userName">The membership user whose lock status you want to clear.</param>
        public override bool UnlockUser(string userName)
        {
            var user = memberRepository.GetByUsername(userName);
            user.IsLockedOut = false;
            user.LastLockedOutDate = DateTime.Now;
            memberRepository.SaveChanges();

            return true;
        }

        #endregion

        #region Get user data

        /// <summary>
        /// Gets the password for the specified user name from the data source.
        /// </summary>
        /// <returns>
        /// The password for the specified user name.
        /// </returns>
        /// <param name="username">The user to retrieve the password for. </param><param name="answer">The password answer for the user. </param>
        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
                throw new ProviderException("Password Retrieval Not Enabled.");

            if (PasswordFormat == MembershipPasswordFormat.Hashed)
                throw new ProviderException("Cannot retrieve Hashed passwords.");

            var user = memberRepository.GetByUsername(username);

            if (null == user)
                throw new MembershipPasswordException("The supplied user name is not found.");

            if (user.IsLockedOut)
                throw new MembershipPasswordException("The supplied user is locked out.");

            if (RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
            {
                UpdateFailureCount(username, "passwordAnswer");
                throw new MembershipPasswordException("Incorrect password answer.");
            }

            string password = user.Password;

            if (PasswordFormat == MembershipPasswordFormat.Encrypted)
                password = UnEncodePassword(password);

            return password;
        }

        /// <summary>
        /// Gets user information from the data source based on the unique identifier for the membership user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name="providerUserKey">The unique identifier for the membership user to get information for.</param><param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user.</param>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            var user = memberRepository.GetById((int)providerUserKey);

            if (null == user)
                return null;

            if (userIsOnline)
            {
                user.LastActivityDate = DateTime.Now;
                memberRepository.SaveChanges();
            }

            return ConvertToMembershipUser(user);
        }

        /// <summary>
        /// Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name="username">The name of the user to get information for. </param><param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. </param>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            var user = memberRepository.GetByUsername(username);

            if (null == user)
                return null;

            if (userIsOnline)
            {
                user.LastActivityDate = DateTime.Now;
                memberRepository.SaveChanges();
            }

            return ConvertToMembershipUser(user);
        }

        /// <summary>
        /// Gets the user name associated with the specified e-mail address.
        /// </summary>
        /// <returns>
        /// The user name associated with the specified e-mail address. If no match is found, return null.
        /// </returns>
        /// <param name="email">The e-mail address to search for. </param>
        public override string GetUserNameByEmail(string email)
        {
            var users = memberRepository.Find(m => m.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
            string username = null;

            if (users.Count() > 0)
            {
                if (users.Count() > 1)
                    throw new ProviderException("Found multiple users for the provided email adress!");
                username = users.Single().Username;
            }

            return username;
        }

        /// <summary>
        /// Gets a collection of all the users in the data source in pages of data.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        /// <param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param><param name="pageSize">The size of the page of results to return.</param><param name="totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var users = memberRepository.GetAll();

            totalRecords = users.Count();
            var resultUsers = new MembershipUserCollection();

            users = users.Skip(pageSize * pageSize).Take(pageSize);
            foreach (var openMembershipUser in users)
            {
                resultUsers.Add(ConvertToMembershipUser(openMembershipUser));
            }

            return resultUsers;
        }

        /// <summary>
        /// Gets the number of users currently accessing the application.
        /// </summary>
        /// <returns>
        /// The number of users currently accessing the application.
        /// </returns>
        public override int GetNumberOfUsersOnline()
        {
            var onlineSpan = new TimeSpan(0, Membership.UserIsOnlineTimeWindow, 0);
            var compareTime = DateTime.Now.Subtract(onlineSpan);

            int numberOfUsersOnline =
                memberRepository.Find(m => m.LastActivityDate > compareTime).Count();

            return numberOfUsersOnline;
        }

        /// <summary>
        /// Gets a collection of membership users where the user name contains the specified user name to match.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        /// <param name="usernameToMatch">The user name to search for.</param><param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param><param name="pageSize">The size of the page of results to return.</param><param name="totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = memberRepository.Find(m => m.Username.Contains(usernameToMatch));
            totalRecords = users.Count();

            var resultUsers = new MembershipUserCollection();
            users = users.Skip(pageSize * pageSize).Take(pageSize);

            foreach (var openMembershipUser in users)
            {
                resultUsers.Add(ConvertToMembershipUser(openMembershipUser));
            }

            return resultUsers;
        }

        /// <summary>
        /// Gets a collection of membership users where the e-mail address contains the specified e-mail address to match.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        /// <param name="emailToMatch">The e-mail address to search for.</param><param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param><param name="pageSize">The size of the page of results to return.</param><param name="totalRecords">The total number of matched users.</param>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = memberRepository.Find(m => m.Email.Contains(emailToMatch));
            totalRecords = users.Count();

            var resultUsers = new MembershipUserCollection();
            users = users.Skip(pageSize * pageSize).Take(pageSize);

            foreach (var openMembershipUser in users)
            {
                resultUsers.Add(ConvertToMembershipUser(openMembershipUser));
            }

            return resultUsers;
        }

        #endregion

        #region Helper functions

        private MembershipUser ConvertToMembershipUser(Member user)
        {
            var convertedUser = new MembershipUser(
                this.Name, user.Username, user.Id, user.Email, user.PasswordQuestion, user.Comment, user.IsApproved,
                user.IsLockedOut, user.CreationDate, user.LastLoginDate, user.LastActivityDate, user.LastPasswordChangedDate, user.LastLockedOutDate);

            return convertedUser;

        }

        /// <summary>
        /// Encrypts, Hashes, or leaves the password clear based on the PasswordFormat.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    encodedPassword =
                      Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    var hash = new HMACSHA1();
                    hash.Key = HexToByte(machineKey.ValidationKey);
                    encodedPassword =
                      Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        /// <summary>
        /// Converts a hexadecimal string to a byte array. Used to convert encryption
        /// key values from the configuration.
        /// </summary>
        /// <param name="hexString">The hex string.</param>
        /// <returns></returns>
        private byte[] HexToByte(string hexString)
        {
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        /// <summary>
        /// Compares password values based on the MembershipPasswordFormat.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <param name="repositoryPassword">The password stored in the repository.</param>
        /// <returns></returns>
        private bool CheckPassword(string password, string repositoryPassword)
        {
            string pass1 = password;
            string pass2 = repositoryPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = UnEncodePassword(repositoryPassword);
                    break;
                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            return pass1 == pass2;
        }

        /// <summary>
        /// A helper method that performs the checks and updates associated with
        /// password failure tracking.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="failureType">Type of the failure.</param>
        private void UpdateFailureCount(string username, string failureType)
        {
            var user = memberRepository.GetByUsername(username);

            DateTime windowStart = new DateTime();
            int failureCount = 0;


            if (failureType == "password")
            {
                failureCount = user.FailedPasswordAttemptCount;
                windowStart = user.FailedPasswordAttemptWindowStart;
            }

            if (failureType == "passwordAnswer")
            {
                failureCount = user.FailedPasswordAnswerAttemptCount;
                windowStart = user.FailedPasswordAnswerAttemptWindowStart;
            }

            DateTime windowEnd = windowStart.AddMinutes(PasswordAttemptWindow);

            if (failureCount == 0 || DateTime.Now > windowEnd)
            {
                // First password failure or outside of PasswordAttemptWindow. 
                // Start a new password failure count from 1 and a new window starting now.

                if (failureType == "password")
                {
                    user.FailedPasswordAttemptCount = 1;
                    user.FailedPasswordAttemptWindowStart = DateTime.Now;
                }

                if (failureType == "passwordAnswer")
                {
                    user.FailedPasswordAnswerAttemptCount = 1;
                    user.FailedPasswordAnswerAttemptWindowStart = DateTime.Now;
                }

                memberRepository.SaveChanges();

            }
            else
            {
                if (failureCount++ >= MaxInvalidPasswordAttempts)
                {
                    // Password attempts have exceeded the failure threshold. Lock out
                    // the user.

                    user.IsLockedOut = true;
                    user.LastLockedOutDate = DateTime.Now;

                    memberRepository.SaveChanges();
                }
                else
                {
                    // Password attempts have not exceeded the failure threshold. Update
                    // the failure counts. Leave the window the same.

                    if (failureType == "password")
                    {
                        user.FailedPasswordAttemptCount = failureCount;
                    }

                    if (failureType == "passwordAnswer")
                    {
                        user.FailedPasswordAnswerAttemptCount = failureCount;
                    }

                    memberRepository.SaveChanges();
                }
            }
        }

        /// <summary>
        /// Decrypts or leaves the password clear based on the PasswordFormat.
        /// </summary>
        /// <param name="encodedPassword">The encoded password.</param>
        /// <returns></returns>
        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        #endregion
    }
}